/**
 * Copyright (C) 2016-2019 DSpot Sp. z o.o
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.dspot.declex.handler.base;

import com.dspot.declex.transform.ClassFieldsTransform;
import com.dspot.declex.transform.ClassFooterTransform;
import com.dspot.declex.transform.ClassHeadTransform;
import com.dspot.declex.transform.HolderMethodTransform;
import com.dspot.declex.util.FileUtils;

import org.ohosannotations.OhosAnnotationsEnvironment;
import org.ohosannotations.annotations.EAbility;
import org.ohosannotations.annotations.EApplication;
import org.ohosannotations.annotations.EBean;
import org.ohosannotations.annotations.EFraction;
import org.ohosannotations.annotations.EService;
import org.ohosannotations.handler.BaseAnnotationHandler;
import org.ohosannotations.holder.BaseGeneratedClassHolder;
import org.ohosannotations.logger.Logger;
import org.ohosannotations.logger.LoggerFactory;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;

import freemarker.template.Configuration;
import freemarker.template.Template;

/**
 * 模板基类Handler
 *
 * @param <T> 子类泛型
 * @since 2021-07-13
 */
public abstract class BaseTemplateHandler<T extends BaseGeneratedClassHolder> extends BaseAnnotationHandler<T> {
    /**
     * 日志打印
     */
    protected static final Logger LOGGER = LoggerFactory.getLogger(BaseTemplateHandler.class);
    /**
     * 目标注释
     */
    protected Class<? extends Annotation> targetAnnotation;
    /**
     * 加工过的持有者
     */
    protected Map<T, Set<String>> processedHolders = new HashMap<>();
    private String templatePath;
    private String templateName;
    private List<String> alreadyWritedTemplates = new ArrayList<>();

    /**
     * 构造函数
     *
     * @param targetClass 目标字节
     * @param environment 注解环境
     * @param templatePath 参数模板路径
     * @param templateName 参数模板名字
     */
    public BaseTemplateHandler(Class<?> targetClass, OhosAnnotationsEnvironment environment,
                               String templatePath, String templateName) {
        super(targetClass, environment);

        this.targetAnnotation = (Class<? extends Annotation>) targetClass;
        this.templatePath = templatePath;
        this.templateName = templateName;
    }

    /**
     * 设置模板数据模型
     *
     * @param rootDataModel 根数据模型
     * @param element 元素
     * @param holder 持有者
     */
    protected void setTemplateDataModel(Map<String, Object> rootDataModel, Element element, T holder) {
        rootDataModel.put("className", holder.getGeneratedClass().name());
        rootDataModel.put("fromClass", element.getSimpleName().toString());

        rootDataModel.put("classNameFull", holder.getGeneratedClass().fullName());
        rootDataModel.put("fromClassFull", element.asType().toString());

        if (element.getKind() == ElementKind.CLASS) {
            if (adiHelper.getAnnotation(element, EAbility.class) != null) {
                rootDataModel.put("classType", "Ability");
            } else if (adiHelper.getAnnotation(element, EFraction.class) != null) {
                rootDataModel.put("classType", "Fraction");
            } else if (adiHelper.getAnnotation(element, EBean.class) != null) {
                rootDataModel.put("classType", "Bean");
            }
            if (adiHelper.getAnnotation(element, EApplication.class) != null) {
                rootDataModel.put("classType", "Application");
            }
            if (adiHelper.getAnnotation(element, EService.class) != null) {
                rootDataModel.put("classType", "Service");
            }
        }

        rootDataModel.put("packageName", getEnvironment().getOhosManifest().getApplicationPackage());
        //Transforms
        rootDataModel.put("holder_method", new HolderMethodTransform<T>(holder));
        rootDataModel.put("class_head", new ClassHeadTransform<T>(holder));
        rootDataModel.put("class_fields", new ClassFieldsTransform<T>(holder));
        rootDataModel.put("class_footer", new ClassFooterTransform<T>(holder));
    }


    @Override
    public void process(Element element, T holder) {
        // This permits to process efficiently abstract classes
        Set<String> targets = processedHolders.get(holder);
        if (targets == null) {
            targets = new HashSet<>();
            processedHolders.put(holder, targets);
        }
        if (targets.contains(getTarget())) return;
        targets.add(getTarget());

        Boolean isCustom = false;

        try {
            Method customMethod = targetAnnotation.getMethod("custom", new Class[]{});
            isCustom = (Boolean) customMethod.invoke(adiHelper.getAnnotation(element, targetAnnotation), new Object[]{});
        } catch (NoSuchMethodException | SecurityException | IllegalAccessException |
            IllegalArgumentException | InvocationTargetException e1) {
            LOGGER.warn(
                "Annotation {} implementation does not includes a \"custom\" method",
                element,
                element.getAnnotation(targetAnnotation)
            );
        }
        int retries = 0;
        while (true) { // On File not found exception, try several times
            try {
                String ftlFileName = templateName;
                String ftlFile = templatePath + ftlFileName;
                LOGGER.info("Starting FTL proccess on {}", ftlFile);

                // Get the file from the package
                URL url = getClass().getClassLoader().getResource(ftlFile);
                if (url == null) {
                    throw new IllegalStateException(ftlFile + " not found, execute ant on the project to generate it");
                }
                String outputDirPath = getProcessingEnvironment().getOptions().get("ftl_source_path");
                if (outputDirPath == null) {
                    outputDirPath = "ftl";
                }

                // Ensure the route to the file be created
                File outputFtl = FileUtils.getConfigFile(outputDirPath, getProcessingEnvironment());
                if (!outputFtl.exists()) {
                    boolean mkdir = outputFtl.mkdir();
                }
                if (isCustom) {
                    String classTypeName = element.asType().toString();
                    String customPath = classTypeName.substring(0, classTypeName.length() - element.getSimpleName().length() - 1);

                    String[] ftlFileSegments = customPath.split("\\.");
                    int ii = 0;
                    while (ii < ftlFileSegments.length) {
                        outputFtl = new File(outputFtl.getCanonicalPath() + File.separator + ftlFileSegments[ii]);
                        if (!outputFtl.exists()) {
                            boolean mkdir = outputFtl.mkdir();
                        }
                        ii++;
                    }

                    ftlFileName = element.getSimpleName() + "_" + ftlFileName;
                } else {
                    String[] ftlFileSegments = templatePath.split("/");
                    int ii = 0;
                    while (ii < ftlFileSegments.length) {
                        outputFtl = new File(outputFtl.getCanonicalPath() + File.separator + ftlFileSegments[ii]);
                        if (!outputFtl.exists()) {
                            boolean mkdir = outputFtl.mkdir();
                        }
                        ii++;
                    }
                }
                boolean writeFtlFile = true;
                String outFileName = outputFtl.getCanonicalPath() + File.separator + ftlFileName;
                File outFile = new File(outFileName);
                if (outFile.exists()) {
                    if (isCustom) writeFtlFile = false;
                    else {
                        if (alreadyWritedTemplates.contains(outFileName)) {
                            writeFtlFile = false;
                        } else {
                            boolean delete = outFile.delete();
                        }
                    }
                }

                if (writeFtlFile) { // Write the file to FTL output folder
                    OutputStream out = new FileOutputStream(outFile);
                    InputStream in = url.openStream();
                    int bb = in.read();
                    while (bb != -1) {
                        out.write(bb);
                        bb = in.read();
                    }
                    out.close();
                    in.close();

                    alreadyWritedTemplates.add(outFileName);
                    LOGGER.info("FTL writed to {}", outFileName);
                }
                Configuration cfg = Configuration.getDefaultConfiguration(); // Adjust the FTL configuration
                cfg.setDirectoryForTemplateLoading(outputFtl);

                Template temp = cfg.getTemplate(ftlFileName); // Create a template

                Map<String, Object> root = new HashMap<>(); // Create a data model
                setTemplateDataModel(root, element, holder);

                StringWriter writer = new StringWriter(); // Merge data model with template
                temp.process(root, writer);
                writer.flush();

                // Write the template output directly to the Generated Class
                holder.getGeneratedClass().direct(writer.toString());
                break;
            } catch (FileNotFoundException error) {
                LOGGER.warn(
                    "FTL Write Error, retry " + (++retries) + ".",
                    element,
                    element.getAnnotation(targetAnnotation)
                );

                if (retries > 5) {
                    LOGGER.error(
                        "FTL PROCESSING ERROR",
                        element,
                        element.getAnnotation(targetAnnotation)
                    );
                }
                continue;
            } catch (Throwable error) {
                LOGGER.error("FTL PROCESSING ERROR", error);
                break;
            }
        }
    }
}
